package com.may.wind.config.security;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.may.wind.encrypt.util.AesUtil;
import com.may.wind.encrypt.util.RsaUtil;
import com.may.wind.entity.User;
import com.may.wind.filter.LoginFilter;
import com.may.wind.pojo.Result;
import com.may.wind.service.UserService;
import org.apache.commons.codec.binary.Base64;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.RememberMeServices;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenBasedRememberMeServices;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import org.springframework.security.web.session.SessionInformationExpiredEvent;
import org.springframework.security.web.session.SessionInformationExpiredStrategy;
import org.springframework.web.cors.CorsUtils;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import javax.sql.DataSource;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    private final String DEFAULT_REMEMBER_ME_KEY = UUID.randomUUID().toString();

    private final int tokenValiditySeconds = 60;

    @Autowired
    UserService userService;

    @Autowired
    private DataSource dataSource; //datasource 用的是springboot默认的application.properties中的配置

    @Autowired
    private ObjectMapper objectMapper;

    private final Logger logger = LoggerFactory.getLogger(SecurityConfig.class);

    /**
     * 密码加密(strength=24 设置加密强度4-31)
     *
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(4);
    }

    /**
     * 自定义登录验证
     *
     * @return
     */
    @Bean
    LoginFilter loginFilter() throws Exception {
        LoginFilter loginFilter = new LoginFilter();
        //设置认证成功返回
        loginFilter.setAuthenticationSuccessHandler(authenticationSuccessHandler());
        //设置认证失败返回
        loginFilter.setAuthenticationFailureHandler(authenticationFailureHandler());
        //这句很关键，重用WebSecurityConfigurerAdapter配置的AuthenticationManager，不然要自己组装AuthenticationManager
        loginFilter.setAuthenticationManager(authenticationManagerBean());
        loginFilter.setRememberMeServices(persistentTokenBasedRememberMeServices());
        return loginFilter;
    }

    /**
     * 身份验证(BCryptPasswordEncoder加密)
     * 配置的是认证信息，AuthenticationManagerBuilder这个类，就是AuthenticationManager的建造者，我们只需要向这个类中配置用户信息，
     * 就能生成对应的AuthenticationManager，这个类是用户身份的管理者，是认证的入口，因此，我们需要通过这个配置，向security提供真实的用户身份。
     *
     * @param auth
     * @throws Exception
     */
    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userService).passwordEncoder(passwordEncoder());
    }

    /**
     * 过滤，授权
     * 配置Security的认证策略
     *
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors()
                .and()
                .authorizeRequests()
                .requestMatchers(CorsUtils::isPreFlightRequest).permitAll() // 处理跨域请求中的Preflight请求
                .antMatchers("/api/getVerifyCode").permitAll()
                .antMatchers("/api/register").permitAll()
                .antMatchers("/api/getPublicKey").permitAll()
                .antMatchers("/swagger-ui.html").permitAll() //允许swagger-ui.html访问
                .antMatchers("/api/getMe").permitAll() // 验证用户是否登录
                .anyRequest().authenticated() // 受限资源
                .and()
                .rememberMe() // 配置rememberMe
                .tokenRepository(persistentTokenRepository())
                .userDetailsService(userService)
                .tokenValiditySeconds(tokenValiditySeconds) // 设置rememberMe失效时间
                .rememberMeServices(persistentTokenBasedRememberMeServices())
                .and()
                .formLogin()
                .loginProcessingUrl("/api/login")
                .and()
                .logout() // 开启注销登录的配置
                .logoutUrl("/api/logout") // 配置注销登录请求url为"/api/logout"，默认为"/logout"
                .clearAuthentication(true) // 清除身份认证信息，默认为true
                .invalidateHttpSession(true) // 是否使session失效，默认为true
                .logoutSuccessHandler(logoutSuccessHandler()) // 注销成功时回调
                .and()
                .addFilterAt(loginFilter(), UsernamePasswordAuthenticationFilter.class)//继承UsernamePasswordAuthenticationFilter，替换原来的UsernamePasswordAuthenticationFilter
                .sessionManagement()
                .maximumSessions(1) // 指定最大登录数
                .maxSessionsPreventsLogin(false) // 是否保留已经登录的用户；为true，新用户无法登录；为false，旧用户被踢出
                .expiredSessionStrategy(sessionInformationExpiredStrategy());

        // 关闭CSRF跨域
        http.csrf().disable();
    }

    /**
     * 静态资源配置
     * 这个配置方法用于配置静态资源的处理方式，可使用 Ant 匹配规则。就是可以不用认证就可以直接访问的接口
     *
     * @param web
     * @throws Exception
     */
    @Override
    public void configure(WebSecurity web) throws Exception {
        // 设置拦截忽略文件夹，可以对静态资源放行
        web.ignoring().antMatchers("/css/**", "/js/**",
                // swagger api json
                "/v2/api-docs",
                // 用来获取支持的动作
                "/swagger-resources/configuration/ui",
                // 用来获取api-docs的URI
                "/swagger-resources",
                // 安全选项
                "/swagger-resources/configuration/security",
                "/swagger-ui.html",
                "/swagger-resources/**",
                "/druid/**");
    }

    @Bean
    public PersistentTokenRepository persistentTokenRepository() {
        JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
        jdbcTokenRepository.setDataSource(dataSource);
        //自动创建相关的token表(首次运行时需要打开，二次运行时需要注解掉)
        //jdbcTokenRepository.setCreateTableOnStartup(true);
        return jdbcTokenRepository;
    }

    /**
     * 自定义RememberMeServices
     *
     * @return
     */
    private RememberMeServices persistentTokenBasedRememberMeServices() {
        return new PersistentTokenBasedRememberMeServices(DEFAULT_REMEMBER_ME_KEY, userDetailsService(), persistentTokenRepository()) {
            @Override
            protected boolean rememberMeRequested(HttpServletRequest request, String parameter) {
                boolean rememberMeFlag = super.rememberMeRequested(request, parameter);
                String RememberMeParameter = super.getParameter();
                String paramValue = (String) request.getAttribute(RememberMeParameter);
                if (paramValue != null) {
                    if (paramValue.equalsIgnoreCase("true") || paramValue.equalsIgnoreCase("on")
                            || paramValue.equalsIgnoreCase("yes") || paramValue.equals("1")) {
                        return true;
                    }
                }
                return rememberMeFlag;
            }
        };
    }

    /**
     * 自定义验证失败处理机制
     *
     * @return
     */
    private AuthenticationFailureHandler authenticationFailureHandler() {
        return new AuthenticationFailureHandler() {
            @Override
            public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException authenticationException) throws IOException, ServletException {
                httpServletResponse.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
                httpServletResponse.setContentType("application/json;charset=UTF-8");
                Map<String, Object> message = new HashMap<>();
                PrintWriter printWriter = httpServletResponse.getWriter();
                message.put("message", authenticationException.getMessage());
                message.put("SPRING_SECURITY_LAST_EXCEPTION", authenticationException.toString());
                try {
                    printWriter.write(writeString(message, httpServletRequest));
                } catch (IOException ioException) {
                    ioException.printStackTrace();
                } catch (Exception exception) {
                    exception.printStackTrace();
                } finally {
                    printWriter.flush();
                    printWriter.close();
                }
            }
        };
    }

    /**
     * 自定义验证成功处理机制
     *
     * @return
     */
    private AuthenticationSuccessHandler authenticationSuccessHandler() {
        return new AuthenticationSuccessHandler() {
            @Override
            public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
                httpServletResponse.setStatus(HttpStatus.OK.value());
                httpServletResponse.setContentType("application/json;charset=UTF-8");
                Map<String, Object> message = new HashMap<>();
                PrintWriter printWriter = httpServletResponse.getWriter();
                User user = (User) authentication.getPrincipal();
                user.setPassword(null);//密码清空
                HttpSession session = httpServletRequest.getSession();
                String sessionId = session.getId();//作为登录标识
                user.setSessionId(sessionId);
                message.put("message", authentication.toString());
                message.put("user", user);
                try {
                    printWriter.write(writeString(message, httpServletRequest));
                } catch (IOException ioException) {
                    ioException.printStackTrace();
                } catch (Exception exception) {
                    exception.printStackTrace();
                } finally {
                    printWriter.flush();
                    printWriter.close();
                }
            }
        };
    }

    /**
     * 自定义注销成功处理机制
     *
     * @return
     */
    private LogoutSuccessHandler logoutSuccessHandler() {
        return new LogoutSuccessHandler() {
            @Override
            public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
                httpServletResponse.setStatus(HttpStatus.OK.value());
                httpServletResponse.setContentType("application/json;charset=UTF-8");
                Map<String, Object> message = new HashMap<>();
                PrintWriter printWriter = httpServletResponse.getWriter();
                message.put("message", authentication.toString());
                message.put("content", String.format("用户%s注销成功！", authentication.getName()));
                try {
                    printWriter.write(writeString(message, httpServletRequest));
                } catch (IOException ioException) {
                    ioException.printStackTrace();
                } catch (Exception exception) {
                    exception.printStackTrace();
                } finally {
                    printWriter.flush();
                    printWriter.close();
                }
            }
        };
    }

    /**
     * 自定义旧用户被踢出后处理方法
     *
     * @return
     */
    private SessionInformationExpiredStrategy sessionInformationExpiredStrategy() {
        return new SessionInformationExpiredStrategy() {
            @Override
            public void onExpiredSessionDetected(SessionInformationExpiredEvent sessionInformationExpiredEvent) throws IOException, ServletException {
                sessionInformationExpiredEvent.getResponse().setStatus(HttpStatus.NETWORK_AUTHENTICATION_REQUIRED.value());
                sessionInformationExpiredEvent.getResponse().setContentType("application/json;charset=UTF-8");
                Map<String, Object> message = new HashMap<>();
                PrintWriter printWriter = sessionInformationExpiredEvent.getResponse().getWriter();
                message.put("message", sessionInformationExpiredEvent.getSessionInformation().toString());
                message.put("content", String.format("已经另一台机器登录，您被迫下线，%tx。", sessionInformationExpiredEvent.getSessionInformation().getLastRequest()));
                try {
                    printWriter.write(writeString(message, sessionInformationExpiredEvent.getRequest()));
                } catch (IOException ioException) {
                    ioException.printStackTrace();
                } catch (Exception exception) {
                    exception.printStackTrace();
                } finally {
                    printWriter.flush();
                    printWriter.close();
                }
            }
        };
    }

    /**
     * Security验证统一返回结果加密
     *
     * @param map                返回数据
     * @param httpServletRequest 前端请求
     * @return 加密数据
     * @throws Exception
     */
    private String writeString(Map<String, Object> map, HttpServletRequest httpServletRequest) throws Exception {
        //前端公钥
        String publicKey = httpServletRequest.getParameter("publicKey");
        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        //每次响应之前随机获取AES的key，加密data数据
        String key = AesUtil.getKey();
        logger.error("AES的key：" + key);
        String dataString = objectMapper.writeValueAsString(Result.ok().data(map));
        logger.error("需要加密的data数据：" + dataString);
        String data = AesUtil.encrypt(dataString, key);

        //用前端的公钥来解密AES的key，并转成Base64
        String aesKey = Base64.encodeBase64String(RsaUtil.encryptByPublicKey(key.getBytes(), publicKey));

        //转json字符串并转成Object对象，设置到Result中并赋值给返回值proceed
        Object proceed = Result.ok().data("result", objectMapper.readValue("{\"data\":\"" + data + "\",\"aesKey\":\"" + aesKey + "\"}", Object.class));
        return objectMapper.writeValueAsString(proceed);
    }
}

